package com.g0dkar.leet.core.configuration;

import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import javax.persistence.Entity;

import com.g0dkar.leet.core.model.ClassMapping;
import com.g0dkar.leet.core.model.Flow;
import com.g0dkar.leet.core.model.Report;
import com.g0dkar.leet.core.model.Section;
import com.g0dkar.leet.core.model.loaders.ClassMappingLoader;
import com.g0dkar.leet.persistence.DummyPersistenceManager;
import com.g0dkar.leet.persistence.PersistenceManager;
import com.g0dkar.leet.util.common.StringUtils;
import com.g0dkar.leet.util.reflection.ReflectionUtils;

/**
 * This class represents the system's configuration. It contains the system's {@link PersistenceManager}, all of the
 * {@link ClassMapping ClassMappings}, {@link Flow Workflows}, {@link Report Reports} and how they're divided (this
 * division is used to determine user access levels and to guide the Leet View Generator).
 * 
 * @author Rafael g0dkar M. Lins
 * 
 */
public abstract class LeetConfiguration implements Serializable {
	/**
	 * Default Serial Version ID: 1L
	 */
	private static final long serialVersionUID = 1L;
	
	private boolean initialized;
	
	private ClassMappingLoader classMappingLoader;
	
	private PersistenceManager persistenceManager;
	
	private Charset pagesCharset;
	private Locale locale;
	
	private String name;
	private String description;
	private String basePackage;
	private String baseDir;
	
	private Map<String, ClassMapping> classMappings;
	private Map<String, Flow> flows;
	private Map<String, Report> reports;
	
	private Map<String, Section> sections;
	
	private Properties properties;
	
	public LeetConfiguration() {
		initialized = false;
		
		classMappings = new HashMap<String, ClassMapping>();
		flows = new HashMap<String, Flow>();
		reports = new HashMap<String, Report>();
		sections = new LinkedHashMap<String, Section>();
		properties = new Properties();
		
		setClassMappingLoader(new ClassMappingLoader());
	}
	
	/**
	 * Initializes this LeetConfiguration instance by setting this application's name, description, base directory, base
	 * package, adding ClassMappings and
	 */
	public abstract void init();
	
	/**
	 * Returns a unique name for this application instance.
	 * 
	 * @return A unique name that identifies this JHeat Application instance.
	 * @see #setName(String)
	 */
	public String getName() {
		return name;
	}
	
	/**
	 * Sets the application unique name.
	 * 
	 * @param name
	 *            - The Application name
	 * @see #getName()
	 */
	public void setName(String name) {
		this.name = name;
	}
	
	/**
	 * Returns a small description of the application
	 * 
	 * @return A small description of the application
	 * @see #setDescription(String)
	 */
	public String getDescription() {
		return description;
	}
	
	/**
	 * Sets the application description.
	 * 
	 * @param description
	 *            - The application description
	 * @see #getDescription()
	 */
	public void setDescription(String description) {
		this.description = description;
	}
	
	/**
	 * Returns the main package of this JHeat Application. All classes will be considered part of this package if they
	 * cannot be found using {@link Class#forName(String)}.
	 * 
	 * @return The main package of the Application
	 * @see #setBaseDir(String)
	 */
	public String getBasePackage() {
		return basePackage;
	}
	
	/**
	 * Sets the main package of this JHeat Application. All classes will be considered part of this package if they
	 * 
	 * @param basePackage
	 *            - The main package of the Application
	 * @see #getBaseDir()
	 */
	public void setBasePackage(String basePackage) {
		this.basePackage = basePackage;
	}
	
	/**
	 * Returns the base directory for this JHeat Application. All files will be considered part of this directory if
	 * they cannot be found using {@link Class#getResource(String)}.
	 * 
	 * @return The base directory of the Application
	 * @see #setBaseDir(String)
	 */
	public String getBaseDir() {
		return baseDir;
	}
	
	/**
	 * Sets the base directory for this JHeat Application. All files will be considered part of this directory if they
	 * cannot be found using {@link Class#getResource(String)}.
	 * 
	 * @param baseDir
	 *            The base directory of the Application
	 * @see #getBaseDir()
	 */
	public void setBaseDir(String baseDir) {
		this.baseDir = baseDir;
	}
	
	/**
	 * The Application's {@link ClassMapping ClassMappings}.
	 * 
	 * @return The Application's ClassMappings
	 * @see ClassMapping
	 * @see #getClassMapping(String)
	 * @see #setClassMappings(Map)
	 * @see #addClassMapping(Class)
	 * @see #addClassMapping(ClassMapping)
	 * @see #addClassMappingPackage(String)
	 */
	public Map<String, ClassMapping> getClassMappings() {
		return classMappings;
	}
	
	/**
	 * Returns the {@link ClassMapping} with the specified name
	 * 
	 * @param name
	 *            - The {@link ClassMapping} name
	 * @see ClassMapping
	 * @see #getClassMappings()
	 * @see #setClassMappings(Map)
	 * @see #addClassMapping(Class)
	 * @see #addClassMapping(ClassMapping)
	 * @see #addClassMappingPackage(String)
	 */
	public ClassMapping getClassMapping(String mappingName) {
		return classMappings.get(mappingName);
	}
	
	/**
	 * Returns the {@link ClassMapping} with the specified name
	 * 
	 * @param name
	 *            - The {@link ClassMapping} name
	 * @see ClassMapping
	 * @see #getClassMappings()
	 * @see #setClassMappings(Map)
	 * @see #addClassMapping(Class)
	 * @see #addClassMapping(ClassMapping)
	 * @see #addClassMappingPackage(String)
	 */
	public ClassMapping getClassMapping(Class<?> klass) {
		for (Entry<String, ClassMapping> entry : classMappings.entrySet()) {
			if (entry.getValue().getType().equals(klass)) {
				return entry.getValue();
			}
		}
		
		return null;
	}
	
	/**
	 * Sets the Application's {@link ClassMapping ClassMappings}.
	 * 
	 * @return The Application's ClassMappings
	 * @see ClassMapping
	 * @see #getClassMapping(String)
	 * @see #getClassMappings()
	 * @see #addClassMapping(Class)
	 * @see #addClassMapping(ClassMapping)
	 * @see #addClassMappingPackage(String)
	 */
	public void setClassMappings(Map<String, ClassMapping> classMappings) {
		this.classMappings = classMappings;
	}
	
	/**
	 * Includes a {@link ClassMapping} on this Application.
	 * 
	 * @param classMapping
	 *            - The new ClassMapping
	 */
	public void addClassMapping(ClassMapping classMapping) {
		classMappings.put(classMapping.getName(), classMapping);
	}
	
	/**
	 * Creates and includes a {@link ClassMapping} for the specified {@link Class}.
	 * 
	 * @param klass
	 *            - The {@link Class} to be mapped
	 */
	public void addClassMapping(Class<?> klass) {
		addClassMapping(getClassMappingLoader().load(klass));
	}
	
	/**
	 * Searches, creates e includes {@link ClassMapping ClassMappings} for all the classes marked as {@link Entity JPA
	 * Entities} found on the <code>packageName</code> package and subpackages.
	 * 
	 * @param packageName
	 *            - The package to search the classes
	 */
	public void addClassMappingPackage(String packageName) {
		Collection<Class<?>> classes = ReflectionUtils.getAllClasses(packageName, null, Entity.class);
		
		if (classes != null && !classes.isEmpty()) {
			for (Class<?> klass : classes) {
				addClassMapping(klass);
			}
		}
	}
	
	/**
	 * Commodity method for {@link #addClassMappingPackage(String)}. Same as
	 * <code>addClassMappingPackage(packageReference.getName())</code>.
	 * 
	 * @param packageReference
	 *            A {@link Package} object.
	 */
	public void addClassMappingPackage(Package packageReference) {
		addClassMappingPackage(packageReference.getName());
	}
	
	public Map<String, Flow> getFlows() {
		return flows;
	}
	
	public void setFlows(Map<String, Flow> flows) {
		this.flows = flows;
	}
	
	public Map<String, Report> getReports() {
		return reports;
	}
	
	public void setReports(Map<String, Report> reports) {
		this.reports = reports;
	}
	
	/**
	 * Returns this Application's Properties (read from the jheat.properties file)
	 * 
	 * @return The Application's Properties
	 * @see Properties
	 * @see Properties#load(java.io.InputStream)
	 */
	public Properties getProperties() {
		return properties;
	}
	
	/**
	 * Set the Application's Properties
	 * 
	 * @param properties
	 *            - The Application's Properties
	 * @see #getProperties()
	 * @see #getProperty(String)
	 * @see #getProperty(String, String)
	 */
	public void setProperties(Properties properties) {
		this.properties = properties;
	}
	
	/**
	 * Gets a property value returning <code>defaultValue</code> if the property is not set.
	 * 
	 * @param key
	 *            The property name
	 * @param defaultValue
	 *            It's default value
	 * @return The property value or <code>defaultValue</code> if it's not set
	 * @see Properties#getProperty(String, String)
	 */
	public String getProperty(String key, String defaultValue) {
		return properties.getProperty(key, defaultValue);
	}
	
	/**
	 * Gets a property value.
	 * 
	 * @param key
	 *            The property name
	 * @return The property value
	 * @see Properties#getProperty(String)
	 */
	public String getProperty(String key) {
		return properties.getProperty(key);
	}
	
	/**
	 * Sets a property value.
	 * 
	 * @param key
	 *            The property name
	 * @param value
	 *            The property value
	 * @return The same value as a {@link Hashtable#put(Object, Object)} call would return
	 * @see Properties#setProperty(String, String)
	 */
	public Object setProperty(String key, String value) {
		return properties.setProperty(key, value);
	}
	
	/**
	 * Returns this Application's Sections
	 * 
	 * @return The Application's Sections
	 */
	public Map<String, Section> getSections() {
		return sections;
	}
	
	/**
	 * Returns a single {@link Section} based on it's name
	 * 
	 * @param name
	 *            - The {@link Section} name
	 * @return The {@link Section} or <code>null</code> if it's not found
	 */
	public Section getSection(String name) {
		return sections.get(name);
	}
	
	/**
	 * Sets the {@link Section Sections} of this Application
	 * 
	 * @param sections
	 *            - The {@link Section Sections}
	 */
	public void setSections(Map<String, Section> sections) {
		this.sections = sections;
	}
	
	/**
	 * Includes a {@link Section} on this Application
	 * 
	 * @param section
	 *            - The new Section
	 */
	public void addSection(Section section) {
		sections.put(section.getName(), section);
	}
	
	/**
	 * Returns a {@link ClassMappingLoader} loader so you can easily create {@link ClassMapping ClassMappings}.
	 * 
	 * @return A {@link ClassMappingLoader}
	 * @see ClassMapping
	 * @see ClassMappingLoader
	 * @see #addClassMapping(Class)
	 */
	public ClassMappingLoader getClassMappingLoader() {
		return classMappingLoader;
	}
	
	/**
	 * Sets the {@link ClassMappingLoader} for this Configuration.
	 * 
	 * @param classMappingLoader
	 *            The {@link ClassMappingLoader} used to load ClassMappings.
	 */
	public void setClassMappingLoader(ClassMappingLoader classMappingLoader) {
		this.classMappingLoader = classMappingLoader;
	}
	
	/**
	 * Returns this application pages' {@link Charset}. The default is <code>UTF-8</code>.
	 * 
	 * @return The {@link Charset}
	 * 
	 * @see Charset#forName(String)
	 */
	public Charset getPagesCharset() {
		return pagesCharset;
	}
	
	/**
	 * Sets this application pages' {@link Charset}. The default is <code>UTF-8</code>.
	 * 
	 * @param pagesCharset
	 *            The new {@link Charset}
	 * @see Charset#forName(String)
	 */
	public void setPagesCharset(Charset pagesCharset) {
		this.pagesCharset = pagesCharset;
	}
	
	/**
	 * Returns this JHeat Application {@link Locale}
	 * 
	 * @return The Application {@link Locale}
	 */
	public Locale getLocale() {
		if (!StringUtils.isBlank(getProperty(LeetConstants.LOCALE))) {
			if (locale == null || !locale.getLanguage().equals(getProperty(LeetConstants.LOCALE))) {
				locale = new Locale(getProperty(LeetConstants.LOCALE));
			}
		}
		else if (locale == null) {
			locale = Locale.getDefault();
		}
		
		return locale;
	}
	
	/**
	 * Returns this application's {@link PersistenceManager}. By default, if this method returns <code>null</code> then
	 * a {@link DummyPersistenceManager} will be used. It sucks, so please avoid using it!
	 * 
	 * @return The PersistenceManager
	 */
	public PersistenceManager getPersistenceManager() {
		return persistenceManager;
	}
	
	/**
	 * Sets this application's {@link PersistenceManager}.
	 * 
	 * @param persistenceManager
	 *            The new {@link PersistenceManager}
	 */
	public void setPersistenceManager(PersistenceManager persistenceManager) {
		this.persistenceManager = persistenceManager;
	}
	
	public boolean isInitialized() {
		return initialized;
	}
	
	public void setInitialized(boolean initialized) {
		this.initialized = initialized;
	}
}
